using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Xml;
using System.Xml.Schema;

namespace FontKonverter {
    /// <summary>
    /// Reads the fontdata from an XML file into a DOM tree
    ///     but creates the data structures for the fonts only on demand.
    /// </summary>
    public class FontDataXML {

        #region Fields

        private readonly int MAXUNI = 0x7f;  // length of unicode table
        private readonly int MAXLEG = 0x100; // length of legacy table
        private readonly int MAXLENGTH = 10; // maximum length of allowed unicode replacement
        private readonly char LEGSEP = ';';  // seperator for legacy attributes

        // cache for the font data
        private Dictionary<string, ArrayList> m_LegacyFontData = null;
        private Dictionary<string, ArrayList> m_UnicodeFontData = null;
        // maps fontnames to fonttypes, which are keys in legacyFontData & unicodeFontData
        private StringDictionary m_FontNames = null;
        // maps fonttypes to DOM tree elements for reading on demand
        private HybridDictionary m_FontElements = null;
        // maps fonttypes to its parents
        private StringDictionary m_Parents = null;


        #region Static Fields

        // List and Check Encoding
        public static readonly string[] EncodingData = new string[] { "cp1252", "utf-8", "latin-1", "iso-8859-1" };
        // List of encodingData for display
        public static readonly string[] ListEncodingTypes = new string[] { "Plain Text (cp1252)", "Plain Text (latin-1/iso-8859-1)", "Unicode (utf-8)" };

        #endregion

        #endregion

        #region .Ctor
        public FontDataXML() {
            this.ReadXML("fontdata.xml");
        }
        #endregion

        #region Process XML font definition file

        ValidationEventHandler validateXMLDataEventHandler = new ValidationEventHandler(FontDataXML.ValidateXMLDataEventHandler);

        public void ReadXML(string filename) {
            XmlDocument xmlDoc = null;
            XmlReader xmlReader = null;
            try {
                XmlReaderSettings settings = new XmlReaderSettings();
                settings.ProhibitDtd = false;
                settings.IgnoreProcessingInstructions = true;
                settings.ValidationType = ValidationType.DTD;
                settings.ValidationEventHandler += validateXMLDataEventHandler;

                xmlReader = XmlReader.Create(filename, settings);
                // Pass the validating reader to the XML document.
                // Validation fails due to an undefined attribute, but the 
                // data is still loaded into the document.
                xmlDoc = new XmlDocument();
                xmlDoc.Load(xmlReader);

                //# Reset all dictionaries
                this.m_FontElements = new HybridDictionary();
                this.m_LegacyFontData = new Dictionary<string, ArrayList>();
                this.m_UnicodeFontData = new Dictionary<string, ArrayList>();
                this.m_FontNames = new StringDictionary();
                this.m_Parents = new StringDictionary();

                XmlNodeList fontElements = xmlDoc.GetElementsByTagName("font");
                if (fontElements == null)
                    throw new ApplicationException("No font data found in " + filename);

                foreach (XmlElement fontElement in fontElements) {

                    string fontType = fontElement.GetAttribute("type").ToLower();
                    if (m_FontElements.Contains(fontType))
                        throw new ApplicationException("Font: " + fontType + " is defined twice in " + filename);

                    string inherit = fontElement.GetAttribute("inherit").ToLower();

                    if (inherit.Length > 0) {
                        if (!m_FontElements.Contains(inherit))
                            throw new ApplicationException("Font " + fontType + " can not inherit unkown font " + inherit + " in " + filename);
                    
                        // map font to parent
                        m_Parents.Add(fontType, inherit);
                    }

                    // map name to element
                    this.m_FontElements.Add(fontType, fontElement);

                    bool hidden = (fontElement.GetAttribute("hidden").ToLower() == "true");
                    if (!hidden) {
                        // add default fonttype to known fontnames
                        this.m_FontNames.Add(this.Beautify(fontType), fontType);
                        
                        // add alias names 
                        XmlNodeList aliasElements = fontElement.GetElementsByTagName("alias");
                        foreach (XmlElement aliasElement in aliasElements) {
                            this.m_FontNames.Add(this.Beautify(aliasElement.GetAttribute("name")), fontType);
                        }
                    }
                }

            } catch (XmlException e) {
                // log error ...
                Console.WriteLine(e.Message + "\r\n" + e.StackTrace);
            } finally {
                if (xmlReader != null)
                    xmlReader.Close();
            }
        }
        private static void ValidateXMLDataEventHandler(object sender, ValidationEventArgs e) {

        }
        #endregion

        #region Public Methods
        /// <summary>
        /// 
        /// </summary>
        /// <returns>return sorted list of font types: ("abc-zwsp", "abc family", "baidok family", "limon family", "fk family", "truth family", "khek family", ...)</returns>
        public string[] ListFontTypes() {
            #region ...
            List<string> ft = new List<string>();
            foreach (string type in this.m_FontNames.Values) {
                if (!ft.Contains(type)) ft.Add(type);
            }

            ft.Sort();
            return ft.ToArray();

            //string[] fontTypes = new string[this.m_FontNames.Count];
            //this.m_FontNames.Values.CopyTo(fontTypes, 0);

            //Array.Sort(fontTypes);

            //return fontTypes;
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>sorted list of all known font names ("Limon S1", "Baidok3c", ...)</returns>
        public string[] ListFontNames() {
            #region ...
            string[] fontNames = new string[this.m_FontNames.Keys.Count];
            this.m_FontNames.Keys.CopyTo(fontNames, 0);
            Array.Sort(fontNames);

            return fontNames;
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontType"></param>
        /// <returns>sorted list of all known font names for a font type</returns>
        public string[] ListFontNamesForType(string fontType) {
            #region ...
            List<string> listFontNames = new List<string>();
            foreach (DictionaryEntry entry in this.m_FontNames) {
                if (entry.Key.ToString().Equals(fontType) && entry.Value.ToString().Equals(fontType))
                    listFontNames.Add(entry.Key.ToString());
            }

            string[] fontNames = new string[listFontNames.Count];
            listFontNames.CopyTo(fontNames);
            Array.Sort(fontNames);

            return fontNames;
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns>return fonttype from fontname</returns>
        public string TypeForFontName(string fontName) {
            #region ...
            fontName = this.Beautify(fontName);
            if (!this.m_FontNames.ContainsKey(fontName))
                throw new ApplicationException("Font: " + fontName + " is unknown.");

            return this.m_FontNames[fontName];
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns>return True if fontname is known, else return False</returns>
        public bool IsConvertable(string fontName) {
            //HACK: raw convert from Python, since Exception is an expensive operation
            try {
                this.TypeForFontName(fontName);
            } catch {
                return false;
            }
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontType"></param>
        /// <returns>return default font name according to fontname</returns>
        public string DefaultFont(string fontType) {
            #region ...
            if (!this.m_FontElements.Contains(fontType))
                return fontType;

            XmlElement element = (XmlElement)this.m_FontElements[fontType];
            string fontName = element.GetAttribute("default");
            if (fontName != null)
                return fontName;

            return fontType;
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns>return data for unicode FontData according to fontname</returns>
        public ArrayList UnicodeData(string fontName) {
            #region ...
            string fonttype;
            try {
                fonttype = this.TypeForFontName(fontName);
            } catch (Exception ex) {
                throw ex;
            }

            //# read if data not available
            if (!this.m_UnicodeFontData.ContainsKey(fonttype))
                this.ReadUnicodeData(fonttype);

            return this.m_UnicodeFontData[fonttype];
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="fontName"></param>
        /// <returns>return data for legacy FontData according to fontname</returns>
        public ArrayList LegacyData(string fontName) {
            #region ...
            string fonttype = "";
            try {
                fonttype = this.TypeForFontName(fontName);   // self.typeForFontname(fontname)
            } catch (Exception ex) {
                throw ex;
            }

            //# read if data not available
            if (!this.m_LegacyFontData.ContainsKey(fonttype))
                this.ReadLegacyData(fonttype);

            return this.m_LegacyFontData[fonttype];
            #endregion
        }

        public bool CanDecode(string encoding) {
            #region ...
            encoding = encoding.ToLower();
            foreach (string st in EncodingData) {
                if (st.Equals(encoding))
                    return true;
            }
            return false;
            #endregion
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sin"></param>
        /// <param name="encoding"></param>
        /// <returns>cp1252 encode string</returns>
        public string ChangeEncoding(string sin, string encoding) {
            if (this.CanDecode(encoding) && !encoding.ToLower().Equals("cp1252")) {
                try {
                    //TODO: porting part...
                    //        sin = sin.decode(encoding)
                    //        sin = sin.encode('cp1252')
                } catch (Exception ex) {
                    throw new ApplicationException("Codec Error " + ex.Message);
                }
            }
            return sin;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Reads the legacy data for one font from the dom tree
        /// </summary>
        /// <param name="fontType"></param>
        private void ReadLegacyData(string fontType) {
            #region ...
            string fonttype = fontType.ToLower();
            if (!this.m_FontElements.Contains(fonttype))
                throw new ApplicationException("Font: " + fontType + " is unknown.");

            XmlElement font = (XmlElement)this.m_FontElements[fonttype];

            StringDictionary legacyDict = new StringDictionary();
            string[] legacyTable = new string[MAXLEG];

            #region check and resolve inheritance
            string parent;
            if (m_Parents.ContainsKey(fonttype)) {
                parent = this.m_Parents[fonttype];
                //# do we need to load the data?
                if (!this.m_LegacyFontData.ContainsKey(parent))
                    this.ReadLegacyData(parent);

                //# copy variables from parent
                StringDictionary tmp = (StringDictionary)this.m_LegacyFontData[parent][0];
                foreach (DictionaryEntry entry in tmp) {
                    legacyDict.Add(entry.Key.ToString(), entry.Value.ToString());
                }

                legacyTable = (string[])this.m_LegacyFontData[parent][1];
            } else {
                //# init variables
                for (int i = 0; i < MAXLEG; i++) {
                    legacyTable[i] = ((char)i).ToString();
                }
            }
            #endregion

            XmlNodeList maps = font.GetElementsByTagName("maps");
            if (maps.Count > 0) {
                this.ReadGlobal((XmlElement)maps[0], legacyTable, legacyDict);
                this.ReadToUnicode((XmlElement)maps[0], legacyDict);
            }

            ArrayList arr = new ArrayList();
            arr.Add(legacyDict);
            arr.Add(legacyTable);

            // boxing
            this.m_LegacyFontData[fonttype] = arr;
            #endregion
        }

        /// <summary>
        /// convert the legacy attribute from number to string
        /// </summary>
        /// <param name="attribute"></param>
        /// <returns>string encoded 'cp1252'</returns>
        private string DecodeLegacy(string attribute) {
            #region ...
            if (attribute.Length == 0)
                return "";
            Console.WriteLine("DecodeLegacy: " + attribute);
            StringBuilder result = new StringBuilder();
            string[] l = attribute.Split(LEGSEP);

            uint uiHex = 0;
            foreach (string piece in l) {
                uiHex = System.Convert.ToUInt32(piece, 16);
                result.Append((char)uiHex);
            }

            return result.ToString();
            #endregion
        }

        /// <summary>
        /// Read the unicode replacements
        /// </summary>
        /// <param name="element"></param>
        /// <param name="unicodeDicts"></param>
        private void ReadFromUnicode(XmlElement element, List<StringDictionary> unicodeDicts) {
            try {
                #region ...
                XmlNodeList fromUnicode = element["fromunicode"].GetElementsByTagName("map");
                if (fromUnicode.Count < 1)
                    return;
                Console.WriteLine("---- ReadFromUnicode ----");
                foreach (XmlElement map in fromUnicode) {
                    string unicode = map.GetAttribute("unicode");
                    string legacy = this.DecodeLegacy(map.GetAttribute("legacy"));
                    int l = unicode.Length;
                    if (l > 0 && l < MAXLENGTH)
                        this.AddToUniData(unicode, legacy, unicodeDicts);
                }
                #endregion
            } catch { }
        }

        /// <summary>
        /// Read the legacy replacements
        /// </summary>
        private void ReadToUnicode(XmlElement element, StringDictionary legacyDict) {
            #region ...
            try {
                XmlNodeList tounicode = element["tounicode"].GetElementsByTagName("map");
                if (tounicode.Count < 1)
                    return;

                Console.WriteLine("---- ReadToUnicode ----");
                foreach (XmlElement map in tounicode) {
                    string legacy = this.DecodeLegacy(map.GetAttribute("legacy"));
                    string unicode = map.GetAttribute("unicode");
                    int l = legacy.Length;
                    if (l > 0 && l < MAXLENGTH)
                        if (!legacyDict.ContainsKey(legacy))
                            legacyDict.Add(legacy, unicode);
                        else
                            throw new ApplicationException("Legacy character " + legacy + " defined twice in toUnicode.");
                }
            } catch { }
            #endregion
        }

        /// <summary>
        /// Read the global replacements for unicode
        /// </summary>
        /// <param name="element"></param>
        /// <param name="unicodeTable"></param>
        /// <param name="unicodeDicts"></param>
        private void ReadGlobalUni(XmlElement element, List<string> unicodeTable, List<StringDictionary> unicodeDicts) {
            try {
                XmlNodeList global = element["global"].GetElementsByTagName("map");
                if (global.Count < 1)
                    return;

                Console.WriteLine("---- ReadGlobalUni ----");
                foreach (XmlElement map in global) {
                    string unicode = map.GetAttribute("unicode");
                    string legacy = this.DecodeLegacy(map.GetAttribute("legacy"));
                    int l = unicode.Length;
                    if (l == 1) {
                        int i = unicode[0] - 0x1780;
                        if (i >= 0 && i < MAXUNI) {
                            if (string.IsNullOrEmpty(unicodeTable[i])) {
                                unicodeTable[i] = legacy;
                            } else {
                                throw new ApplicationException("Unicode character " + unicode + " defined twice in global.");
                            }
                        } else {
                            this.AddToUniData(unicode, legacy, unicodeDicts);
                        }
                    } else {
                        if (l > 1 && l < MAXLENGTH)
                            this.AddToUniData(unicode, legacy, unicodeDicts);
                    }
                }
            } catch { }
        }

        /// <summary>
        /// Read the global replacements for legacy
        /// </summary>
        /// <param name="element"></param>
        /// <param name="legacyTable"></param>
        /// <param name="legacyDict"></param>
        public void ReadGlobal(XmlElement element, string[] legacyTable, StringDictionary legacyDict) {
            try {
                XmlNodeList global = element["global"].GetElementsByTagName("map");
                if (global.Count < 1)
                    return;

                Console.WriteLine("---- ReadGlobal ----");
                foreach (XmlElement map in global) {
                    #region Loading mapping ...
                    string legacy = this.DecodeLegacy(map.GetAttribute("legacy"));
                    string unicode = map.GetAttribute("unicode");

                    int l = legacy.Trim().Length;
                    if (l == 1) {
                        ///HACK: need to confirm ...
                        int i = (Char)legacy[0];                //# i = ord(legacy)
                        if (i >= 0 && i < MAXLEG) {
                            if (legacyTable[i].Equals(legacy)) {
                                legacyTable[i] = unicode;
                            } else {
                                throw new ApplicationException("Legacy character " + i + " defined twice in global.");
                            }
                        }
                    } else if (l > 0 && l < MAXLENGTH) {
                        if (!legacyDict.ContainsKey(legacy))
                            legacyDict.Add(legacy, unicode);
                        else
                            throw new ApplicationException("Legacy character " + legacy + " defined twice in global.");
                    }
                    #endregion
                }
            } catch { }
        }

        /// <summary>
        /// put the unicode to legacy mapping in the right dict.
        /// data will get new dicts if needed
        /// </summary>
        /// <param name="legacy"></param>
        /// <param name="data"></param>
#if DEBUG
        public void AddToUniData(string unicode, string legacy, List<StringDictionary> data)
#else
        private void AddToUniData(string unicode, string legacy, List<StringDictionary> data)
#endif
 {
            int l = unicode.Length;
            //# sanity check 
            //TODO: make this method private when deploy ...
            if (l > 0 && l < MAXLENGTH) {
                //# make sure we have enough dict's    
                while (data.Count < l)
                    data.Add(new StringDictionary());

                //# insert into dict
                if (!data[l - 1].ContainsKey(unicode)) {
                    data[l - 1][unicode] = legacy;
                } else {
                    throw new ApplicationException("Unicode string " + unicode + " already in datastructure.");
                }
            }
        }

        /// <summary>
        /// Reads the unicode data for one font from the dom tree
        /// </summary>
        /// <param name="fontType"></param>
        /// <returns></returns>
        private void ReadUnicodeData(string fontType) {
            string fonttype = fontType.ToLower();
            if (!this.m_FontElements.Contains(fonttype))
                throw new ApplicationException("Font: " + fontType + " is unknown.");

            XmlElement font = (XmlElement)this.m_FontElements[fonttype];
            List<StringDictionary> unicodeDicts = new List<StringDictionary>();
            List<string> unicodeTable;
            // check and resolve inheritance
            if (this.m_Parents.ContainsKey(fonttype)) {
                string parent = this.m_Parents[fonttype];
                // do we need to load the data?
                if (!this.m_UnicodeFontData.ContainsKey(parent))
                    this.ReadUnicodeData(parent);

                //TODO: copy variables from parent
                List<StringDictionary> tmp = (List<StringDictionary>)this.m_UnicodeFontData[parent][0];
                foreach (StringDictionary entry in tmp) {
                    unicodeDicts.Add(entry);
                }
                unicodeTable = (List<string>)this.m_UnicodeFontData[parent][1];
            } else {
                // ini variables
                unicodeDicts = new List<StringDictionary>();
                unicodeTable = new List<string>();
                for (int i = 0; i < MAXUNI; i++) {
                    unicodeTable.Add("");
                }
            }

            XmlNodeList maps = font.GetElementsByTagName("maps");
            if (maps.Count > 0) {
                this.ReadGlobalUni((XmlElement)maps[0], unicodeTable, unicodeDicts);
                this.ReadFromUnicode((XmlElement)maps[0], unicodeDicts);
            }

            ArrayList arr = new ArrayList();
            arr.Add(unicodeDicts);
            arr.Add(unicodeTable);

            this.m_UnicodeFontData.Add(fonttype, arr);
        }

        /// <summary>
        /// lowercase and no dash, no underscore
        /// </summary>
        /// <param name="fontname"></param>
        /// <returns></returns>
        private string Beautify(string fontname) {
            return fontname.ToLower().Replace("-", " ").Replace("_", " ");
        }
        #endregion

    }
}
